feat(devcontainer): un poste de developpement qui n'installe rien - #55
Merged
Conversation
…onception
La question était « WSL + devcontainer, sans outils sur le poste, Windows et
Linux : possible ? ». Oui, et ce document nomme la frontière plutôt que de
promettre l'équivalence.
Six des sept vérifications de la CI se rejouent dans le conteneur. La septième
— PowerShell 5.1 — ne le peut pas, et c'est écrit noir sur blanc : « estimer que
les scripts sont corrects » est la faute exacte qui a livré la v0.1.
Trois constats qui viennent du code et non d'une opinion :
- Le matériel n'entre pas dans la question. serial.Opener est une seam
injectée, et hardware_test.go pose qu'une machine sans port série est « le
cas de développement ORDINAIRE ». Un conteneur donne ce que donne un poste
dont la balance est débranchée.
- Le conteneur RAJOUTE deux gardes à un développeur Windows : la passe -race,
qui se saute faute de gcc, et TestADirectoryTheServiceCanReadButNotWriteIsRefused,
qui saute sous Windows par nature — à condition que remoteUser ne soit pas
root, sans quoi ce banc disparaît en silence.
- Un devcontainer.json qui recopie Go 1.26.5, Node 22 et Python 3.13 en ferait
un quatrième endroit où ces numéros vivent. SUIVI.md rappelle que le seul
compteur d'ADR a menti trois fois pour cette raison : deploy/devcontainer_test.go
les compare à go.mod, ci.yml et docs.yml dans les deux sens.
L'emplacement du dépôt sous Windows est tranché sur une mesure et non sur un
adjectif : 143 ms contre 5 ms pour parcourir 577 fichiers, soit x29. Le bind
Windows reste le chemin par défaut — c'est ce qu'un contributeur fera sans rien
lire — et le clone côté WSL est documenté comme accélérateur.
Ni le Makefile ni make.ps1 ne bougent : le devcontainer est une seconde porte.
Cinq tâches. Le banc anti-dérive est écrit AVANT les fichiers qu'il garde, sur le modèle de tools/deps : sa première exécution est rouge, et ce sont les tâches suivantes qui la font passer au vert. Un banc écrit après un fichier correct ne dit jamais s'il rougirait le jour où le fichier cesse de l'être. Le lecteur de JSONC est une tâche à lui seul parce qu'il porte le seul piège non trivial du lot : un « // » à l'intérieur d'une chaîne n'est pas un commentaire, et un lecteur naïf coupe « https://containers.dev » en deux pour livrer à json.Unmarshal une chaîne non terminée. Le banc vit dans deploy/ et non dans .devcontainer/ : l'outil Go ignore les répertoires commençant par un point, et un test qui ne s'exécute jamais passe pour un vert. La tâche 4 ne se coche pas sur une sortie supposée. Elle casse le banc exprès — 1.26.5 en 1.26.6, vscode en root — pour le voir rougir, et vérifie le même fichier depuis un hôte Linux, où un décalage d'UID ferait apparaître tout le dépôt comme modifié.
devcontainer.json est du JSONC, et ce dépôt commente ses fichiers de configuration. encoding/json refuse un commentaire : il faut donc les retirer avant de décoder. Le lecteur suit l'état des chaînes plutôt que de chercher deux caractères. Sans cela, « https://containers.dev » serait coupé en deux et json.Unmarshal répondrait « unterminated string » en nommant une ligne — on chercherait la faute dans le mauvais fichier.
… les versions Go 1.26.5 vit dans go.mod et ci.yml, Node 22 dans ci.yml, Python 3.13 dans docs.yml, golangci-lint v2.12.2 dans le Makefile — chacun à un seul endroit, et la CI lit le dernier plutôt que de le recopier. Un devcontainer.json qui réécrirait ces numéros en ferait un quatrième endroit ; SUIVI.md rappelle que le seul compteur d'ADR a menti trois fois pour cette raison. Le banc compare dans les deux sens, et il exige aussi remoteUser non root : TestADirectoryTheServiceCanReadButNotWriteIsRefused saute sous root ET sous Windows, si bien qu'un conteneur root laisserait cette branche couverte par rien tout en restant vert. Rouge à ce commit : .devcontainer/ n'existe pas encore. C'est voulu — un banc écrit après le fichier qu'il garde ne dit jamais s'il rougirait.
…se rejouent Trois fichiers : l'image et ses trois paquets apt, la déclaration, et ce qui s'installe après la construction. Chaque paquet tient un banc et le dit : build-essential pour gcc, sans quoi la passe -race se saute ; zip pour la cible release ; systemd pour systemd-analyze seul. Le banc du commit précédent refuse qu'on allège cette liste en silence. Les caches Go et npm sont dans des volumes, hors du dossier monté : sous Windows, le bind coûte ×29 sur les métadonnées — 143 ms contre 5 ms pour parcourir 577 fichiers. Sortis du bind, ils ne paient plus cette taxe. post-create.sh lit la version de golangci-lint par « make -s golangci-version » et l'installe hors module, comme ADR-039 l'exige et comme le fait déjà ci.yml.
…aient faux Le chown de post-create.sh ne portait que sur go-build ; $HOME/.cache reste un parent que Docker crée sous root, et c'est là que golangci-lint écrit. Le chown couvre maintenant tout $HOME/.cache. apt-get install manquait DEBIAN_FRONTEND=noninteractive : dbus ou libpam-systemd, tirés par systemd, peuvent poser une question debconf, et une invite dans docker build fige la construction au lieu d'échouer. Placé dans le RUN, pas en ENV, pour ne pas fuiter dans le conteneur en marche. Quatre commentaires corrigés pour dire le vrai mécanisme : build-essential fournit aussi make, dont post-create.sh dépend directement ; le banc ne compare que trois versions et interdit l'écriture de la quatrième, il ne les compare pas toutes ; npm ci est choisi pour son échec déterministe sur un lock désynchronisé, pas parce qu'un « ^ » bougerait un fichier que ci ne toucherait pas davantage ; et le shebang de post-create.sh n'est jamais consulté puisque devcontainer.json l'invoque par bash, pas par exécution directe — la règle LF reste vraie, seul le mécanisme de panne cité était emprunté à install.sh.
Dans le devcontainer, le dépôt est monté par bind depuis l'hôte Windows, et NTFS présente chaque fichier comme exécutable et inscriptible par tous à Linux. systemd-analyze verify refusait donc les deux unités sur leur MODE de fichier, jamais sur leur contenu, ce qui faisait rougir TestTheUnitIsValidAccordingToSystemdItself sans qu'aucune unité ne soit en cause. Le banc copie désormais les deux unités dans un répertoire temporaire avec le mode 0644, puis fait vérifier les copies par systemd-analyze. Il juge ainsi le contenu des unités, indépendamment du système de fichiers qui héberge le checkout.
`ghcr.io/devcontainers/features/go:1` est un tag MOBILE, et ci.yml dit déjà ce que ça vaut : « un tag n'est pas une version, c'est un pointeur — et sur un dépôt public, c'est le chemin d'attaque le moins coûteux qui existe contre une chaîne de construction ». Le raisonnement qui épingle chaque action GitHub sur un SHA de commit s'applique mot pour mot aux features du conteneur. Ce fichier est produit par le CLI et non écrit à la main. Il ne déplace aucune version : devcontainer.json continue de déclarer Go 1.26.5, Node 22 et Python 3.13, que deploy/devcontainer_test.go compare à go.mod, ci.yml et docs.yml. Ce qu'il fige, c'est la révision des features elles-mêmes.
Sur un poste Windows, le workspace est monte tel quel et tout y appartient a root alors que post-create.sh tourne sous vscode : git refuse le depot pour « dubious ownership », et `go list` perd son horodatage VCS sur ce meme refus. Le premier symptome vu ne nomme ni git ni les droits d'acces : c'est « boundary: 1 violation(s) — voir docs/02-architecture.md §5.2 », qui fait mourir `make test` dans `make boundary`. Touche tout contributeur passant par le chemin par defaut (cloner sous Windows, rouvrir dans le conteneur). Un banc dans deploy/devcontainer_test.go garde la ligne en place : verifie rouge sans elle puis vert avec, avant ce commit.
…sion « vscode » dans remoteUser est le nom d'un compte Unix livré par l'image de base, sans rapport avec l'éditeur — une question réelle du propriétaire du produit montre que ce n'était pas évident à la lecture. Le bloc customizations.vscode.extensions n'avait aucune mention de sa portée : un contributeur qui n'ouvre pas VS Code pouvait se demander si sa présence rendait l'éditeur obligatoire. Elle ne le rend pas : la clé est ignorée par tout ce qui n'est pas VS Code ou l'un de ses forks.
getting-started.md ouvre sur deux chemins au lieu d'un seul. Le tableau des prérequis existant ne bouge pas : il devient le chemin local, qui reste la référence. Le chemin conteneur mène avec la CLI `devcontainer`, indépendante de tout éditeur — c'est elle qui a servi à construire, lancer et tester cette fonctionnalité de bout en bout, sans jamais ouvrir VS Code. Un éditeur qui la porte (VS Code, Cursor, Windsurf, une JetBrains récente) n'a besoin que de Docker ; la CLI demande en plus Node sur le poste. Les deux coûts mesurés tiennent dans une note : 8 min 3 s pour la première image, 84 s pour `make test` dedans, passe `-race` comprise. Deux phrases devenaient fausses telles quelles — « pas de Docker » ici et dans le README. Elles disaient qu'aucun outil n'est requis, ce qui reste vrai : elles le disent maintenant en nommant la seconde porte. Ce que le conteneur ne juge pas tient en une phrase et pas en un paragraphe : les scripts d'installation sous Windows PowerShell 5.1, rendus par le job « scripts » de la CI à chaque pull request.
…n seul « Comptez cinq minutes » ouvrait la page avant toute bifurcation, alors que le chemin conteneur — présenté en premier — coûte 8 min 3 s à la première construction, mesurés. Le lecteur ne l'apprenait que quarante lignes plus bas. La phrase annonce désormais les deux, et dit que la note salée n'est payée qu'une fois.
Le banc anti-dérive avait trois angles morts. F1 : rien ne vérifiait que devcontainer.json construit encore l'image depuis le Dockerfile qu'il inspecte — un « image » préconstruit aurait laissé les sept tests verts pendant que gcc, zip et systemd disparaissaient. F2 : la disparition du feature powershell se traduit par un skip « ni pwsh ni powershell » qui ressemble à une machine sans Windows, jamais à une perte. F3 : le fichier de verrou devcontainer-lock.json n'était gardé par rien — le supprimer ou y oublier un feature laissait tout vert. F6 corrige aussi un banc existant : strings.Contains(dockerfile, "zip") est satisfait par gzip ou bzip2. Un \b le ferme. F9 étend le banc `sh -n` de deploy/shell_test.go à .devcontainer/post-create.sh, jusque-là non analysé : une faute de syntaxe n'y aurait été découverte qu'après une construction de huit minutes.
…commentaire faux
F4 : source=openscale-node-modules était un nom fixe. Deux conteneurs
OpenScale ouverts à la fois — un arbre de revue et le clone principal —
partageaient un seul web/node_modules pour deux package-lock.json
potentiellement différents ; le `npm ci` de l'un vidait celui de l'autre,
sans qu'aucun message ne parle de montage. ${devcontainerId} isole désormais
ce volume par conteneur. GOMODCACHE et GOCACHE restent partagés : leur
contenu est adressé par version et l'outil Go verrouille ce qu'il lit, le
partage n'y est qu'un gain — la distinction est dans le commentaire.
F5 : le Dockerfile affirmait que post-create.sh appelle
`make -s golangci-version` en première commande réelle ; ce sont
`git config` puis `sudo chown` qui s'exécutent avant. L'argument de fond
(retirer ce paquet à gcc seul casserait le script, qui a besoin de make)
reste, reformulé sans l'affirmation fausse.
F7 : sous `set -e`, un `go install` en échec sortait le script avant le
`rm -rf "$install_dir"` de la ligne suivante, laissant le répertoire
temporaire. Un `trap ... EXIT` posé juste après le `mktemp -d` couvre les
deux chemins.
F8 : `npm ci --prefix web` s'aligne désormais sur `npm --prefix web ci`,
l'ordre que le Makefile emploie partout — un grep trouve maintenant tous
les appels npm avec la même forme.
…ier de verrou §5.1 : l'image de base reste sur l'étiquette mobile ubuntu-24.04 quand les features sont épinglées par empreinte — deux arbitrages du propriétaire du produit, chacun sa raison, qui coexistaient jusque-là sans qu'aucun des deux ne soit écrit. §5.2 : devcontainer-lock.json n'était mentionné nulle part dans la conception. Le paragraphe ajouté dit ce qu'il est (produit par la CLI, jamais écrit à la main), pourquoi il est committé (dépôt public, features/go:1 est une étiquette mobile) et comment le rafraîchir (`devcontainer upgrade`) — sans quoi un fichier généré se fait un jour supprimer comme un artefact.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Un contributeur qui n'a que Docker peut désormais travailler sur ce dépôt : Go 1.26.5, Node 22, Python 3.13, gcc, PowerShell 7 et golangci-lint, tous aux versions que la CI épingle, sans rien installer d'autre sur son poste. Fonctionne sous Windows et sous Linux avec le même fichier.
La frontière, et elle est écrite
Six des sept vérifications de la CI se rejouent dans le conteneur : la passe
-race, la passe sans cgo, les garde-fous, la compilation croisée, l'écran client et le handbook.La septième ne le peut pas. Windows PowerShell 5.1 n'existe pas dans un conteneur Linux, et « estimer que les scripts sont corrects » est exactement la faute qui a livré la v0.1. Le job
scriptsdeci.ymlreste le seul juge, à chaque pull request. La documentation le dit en une phrase plutôt que de le laisser deviner.Le matériel ne pose pas de question : aucun test n'ouvre de port série —
serial.Openerest une seam injectée, ethardware_test.gopose qu'une machine sans port série est « le cas de développement ORDINAIRE ». Un conteneur donne ce que donne un poste dont la balance est débranchée.Ce que le conteneur rend à un développeur Windows
Deux bancs qu'un poste Windows n'exécute jamais :
-race, qui se saute faute de gcc ;TestADirectoryTheServiceCanReadButNotWriteIsRefused, qui saute sous Windows (« un répertoire Windows se ferme par une ACL et non paros.Chmod») et sous root — d'où leremoteUsernon root, sans quoi ce banc disparaîtrait en silence.Le banc anti-dérive
deploy/devcontainer_test.go— onze bancs. Aucune version n'est décidée dans.devcontainer/: Go vient dego.modet deci.yml, Node deci.yml, Python dedocs.yml, golangci-lint duMakefileparmake -s golangci-version. Le banc les compare dans les deux sens et refuse qu'un numéro soit recopié.SUIVI.mdrappelle que le seul compteur d'ADR a menti trois fois pour cette raison.Il garde aussi ce qui disparaîtrait sans bruit : que l'image est bien construite depuis le
Dockerfileinspecté, que le featurepowershellest là, que chaque feature est épinglée par empreinte dans le fichier de verrou, et que les trois paquetsapt— chacun attaché nommément au banc qu'il maintient en vie — n'ont pas été rognés.Chaque banc a été vu rouge avant d'être commité. Une garantie qu'on n'a pas vue échouer n'est pas une garantie.
Deux correctifs que seule la construction réelle pouvait trouver
deploy/linux_test.go— le montage bind depuis Windows présente tout en 0777, etsystemd-analyzerefusait alors les unités sur leur mode de fichier, jamais sur leur contenu. Le banc vérifie désormais une copie à 0644. Il juge le contenu, seule chose que le checkout contrôle :install.shposeinstall -m 0644à l'installation, donc le mode du checkout n'atteint jamais la production.post-create.sh— le même montage fait apparaître.gitcomme appartenant à root, git refuse le dépôt,go listperd l'estampille VCS, etmake testmeurt dansmake boundarysur « 1 violation(s) — voir §5.2 », qui ne mentionne ni git ni les permissions.safe.directoryreferme la chaîne, et un banc refuse son retrait.Mesuré, pas promis
make testdedans,-racecomprisemake front-checkinternal/web/distreste identique à l'octetsudo,git statusvideLes caches Go et npm sont dans des volumes hors du montage : seule la lecture des sources paie cette taxe.
Ce qui n'est pas fait, et pourquoi
Pas de passthrough série — aucun test n'en a besoin. Pas de job CI qui construit l'image — le banc attrape la dérive de version pour quelques millisecondes, une construction coûterait cinq à six minutes par pull request. Ni le
Makefilenimake.ps1ne changent : le chemin sans conteneur reste la référence, et c'est lui que la CI exécute. Le devcontainer est une seconde porte.Documentation
handbook/getting-started.mdouvre sur deux chemins. Le tableau des prérequis actuel est conservé intact comme chemin local. Le chemin conteneur mène par la CLIdevcontainer, indépendante de l'éditeur — et c'est celui qui a réellement servi à construire et vérifier ce lot. Le compromis est dit franchement : un éditeur intégré ne demande que Docker, la CLI demande Docker et Node.Conception et arbitrages :
docs/superpowers/specs/2026-08-11-devcontainer-poste-sans-outillage-design.md.